前一篇稍微的介紹了 GraphQL 裡面的 Schema 以及 Type,這一篇將會進一步嘗試更複雜的 Schema,介紹一些 Query 的變化,以及 GraphQL 的一些特色。
延續上一篇的內容,把 GraphQLSchema.js
裡面的 User
、Post
兩種 Type 定義好,並讓完整的清單能在 Query 上面找得到:
exports.schema = buildSchema(`
type User {
id: ID!
name: String!
}
type Post {
id: ID!
title: String!
body: String!
}
type Query {
users: [User!]!
posts: [Post!]!
}
`);
接著我們用最簡單的方式來把資料存在對應的 JavaScript 變數中:
const usersById = {
1: {
id: 1,
name: 'chentsulin',
},
};
const postsById = {
18: {
id: 18,
authorId: 1,
title: 'Day 18:GraphQL 入門 Part I - 從 REST 到 GraphQL',
body: 'Facebook 在 2012 年開始在公司內部使用 GraphQL,而在 2015 年 7 月開源...',
},
19: {
id: 19,
authorId: 1,
title: 'Day 19:GraphQL 入門 Part II - 實作 Schema & Type',
body: '前一篇講了 REST 的一些缺點,還有 GraphQL 如何解決這些問題...',
},
};
有了資料後再改寫 rootValue
讓 defaultFieldResolver
能處理 users
、post
這兩個 Key:
exports.rootValue = {
users: () => Object.keys(usersById).map(id => usersById[id]),
posts: () => Object.keys(postsById).map(id => postsById[id]),
};
就能在 GraphiQL 用以下的 Query 來看到結果:
query {
users {
id
name
}
posts {
id
title
body
}
}
前面的 query
可以省略不寫,所以我們接下來幾乎都會省略:
{
users {
id
name
}
posts {
id
title
body
}
}
GraphQL 的強項之一就是處理關聯的複雜度,而前面的範例其實還沒有真的實作到關聯的部分,雖然眼尖的讀者可能會發現,我在 Post 的資料上面留下了 authorId
這個屬性。
而 GraphQL 完全不限制拿資料的方式,不管是要從變數、SQL、NoSQL、RESTful API 拿,都是可行的。也必須特別強調,GraphQL 的 Type 不一定要跟資料庫表格有明確的對應。
這次我們修改一下 Schema,在 User
跟 Post
之間建立一個一對多的關聯,可以查 User
的所有 posts
,也可以查 Post
的 author
:
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
author: User!
title: String!
body: String!
}
針對 Schema 上面所定義的屬性 posts
跟 author
,已經不是用原始的 JavaScript 物件就能表達的了,所以必須宣告特別的 class
來處理 Resolve 的邏輯,下方的 GraphQLUser.posts
跟 GraphQLPost.author
先留空沒關係:
class GraphQLUser {
constructor({ id, name }) {
this.id = id;
this.name = name;
}
posts() {
// ...待實作
}
}
class GraphQLPost {
constructor({ id, authorId, title, body }) {
this.id = id;
this.authorId = authorId;
this.title = title;
this.body = body;
}
author() {
// ...待實作
}
}
接著把原本 Resolve 的一般物件替換成用 class 建構出來的物件:
exports.rootValue = {
users: () => Object.keys(usersById).map(
id => new GraphQLUser(usersById[id])
),
posts: () => Object.keys(postsById).map(
id => new GraphQLPost(postsById[id])
),
};
如此一來應該不會破壞原本的任何功能,因為在 GraphQLUser
上一樣能取到 id
跟 name
!
再來要實作 GraphQLPost.author
的 Resolve 邏輯,利用 authorId
去把資料取回來並回傳 GraphQLUser
:
class GraphQLPost {
//...
author() {
return new GraphQLUser(usersById[this.authorId]);
}
}
GraphQLUser.posts
的 Resolve 邏輯則複雜一點,要用 id
過濾出屬於 User 的文章:
class GraphQLUser {
//...
posts() {
return Object.keys(postsById)
.map(id => new GraphQLPost(postsById[id]))
.filter(post => post.authorId === this.id);
}
}
完成這個步驟就已經算是大功告成,可以開始嘗試最恐怖的巢狀 Query:
{
users {
name
posts {
title
author {
name
posts {
title
author {
id
name
}
}
}
}
}
}
很順的拿回了結果!這就是 GraphQL 最令人驚艷的地方。
如同 REST 的 GET
一樣,在使用 Query 時難免還是需要設定參數,而這在 GraphQL 是透過在 Key 後面放置 ()
來傳遞,例如:
{
user(id: 1) {
name
}
}
實作上也非常簡單,如下方這樣去定義把 id: ID!
傳遞給 user
的 Resolving Function:
type Query {
users: [User!]!
posts: [Post!]!
user(id: ID!): User
}
就可以在第一個參數拿到 id
:
exports.rootValue = {
// ...
user: ({ id }) => usersById[id] ? new GraphQLUser(usersById[id]) : null,
};
在 GraphiQL 上應該能得到以下結果:
{
user(id: 1) {
name
posts {
title
}
}
}
GraphQL 是個剛起步的生態系,雖然套件的完善程度可能遠不及 REST 的千分之一,但也算是發展的非常迅速,在這幾篇中使用到的 GraphiQL 當然也是其中之一。
Lokka 是個小型的 GraphQL Client,只有簡單的 Query、Mutation、Fragemt 的功能以及非常陽春的 Cache 功能,不過也是最好上手的,彈性高。
Apollo Client 則是中型的 GraphQL Client,支援 React、Vue、Angular 2 等等的框架。它是由目前 GraphQL 最大的社群組織 - Apollo 所維護,這個社群組織囊跨前後端以及工具的開發,並擁抱開源,他們最早的架構設計圖稿也能在 Github 上看到。
Relay 則是由 Facebook 所開發目前最有野心的一個 GraphQL Client,除了跟 React 密切的結合外,還幫忙處理了 Cache、Fragment Composition、Pagination、錯誤處理、Optimistic Updates 等等最困難的問題。
跟以往開發不同的是,在寫 GraphQL 時最多的時間會花在定義Schema 跟 Type,雖然乍看之下定義會花掉很多很多的時間,但定義完後就能隨便查詢的人怎麼去使用它,並盡其所能地去利用系統中所有能查詢的資料,這還算是值得的。至於如果效能上的擔憂,建議看一下 Facebook 內部搭配 GraphQL 所使用的機制 DataLoader,應該能解除你的疑慮。